目前的遊戲缺少聲音還是少了點臨場感,玩遊戲的音效還是蠻重要的,當你操作角色做出一些行為,例如攻擊敵人時,會有攻擊的音效,才會有點回饋感。
而 Kenney 所提供的資源也很多,連音效都有提供,可以去實際每一個試聽一下。
我挑了幾個音效檔案放在 assets/sounds/
中,目前先實作有關玩家跟敵人還有音效(Sound Effects)。把這些整理好的素材正式接到遊戲裡,讓戰鬥、撿寶、升級甚至開門都有對應音效。
今天設定的重點:
AudioPlugin
,監聽事件後以一次性 AudioPlayer
播放音效。這次先新增 new_demo/src/resources/sound_effects.rs
,內容是一個單純的 SoundEffects
資源。把玩家攻擊、撿拾、升級、持續受傷,以及敵人攻擊、門開門/關門等音效 Handle 通通集中在一起:
#[derive(Resource)]
pub struct SoundEffects {
pub player_attack: Handle<AudioSource>,
pub player_pickup: Handle<AudioSource>,
pub player_upgrade: Handle<AudioSource>,
pub player_hurt: Handle<AudioSource>,
pub enemy_attack: Handle<AudioSource>,
pub door_open: Handle<AudioSource>,
pub door_close: Handle<AudioSource>,
}
搭配的 load_sound_effects
寫在 new_demo/src/systems/audio.rs
,它會在 Startup 階段依序呼叫 asset_server.load()
把檔案讀進來,最後插入 SoundEffects
資源。後面所有播放邏輯只需要 clone Handle,不再重複查字串或重新載入。
這次的重構重點在於事件。每個會觸發音效的行為,都對應一個事件型別:
PlayerMeleeAttackEvent
,音效系統只要監聽即可。PlayerPickupEvent
,地上撿拾與寶箱獲得道具都發這個事件。(new_demo/src/systems/items.rs
, new_demo/src/systems/chest.rs
)PlayerPoisonDamageEvent
確保毒傷 tick 時會再送一個事件給音效層。(new_demo/src/systems/player_status.rs
)EnemyAttackHitEvent
在 enemy_contact_attack_system
確認扣血後送出。(new_demo/src/systems/enemy.rs
)DoorStateChangedEvent
讓音效系統知道門目前是開還是關,好切換對應檔案。(new_demo/src/systems/door_interaction.rs
)PlayerLevelUpEvent
,升級時播 fanfare。對應的 plugin 也各自 add_event
,把事件註冊進 App(例如 PlayerPlugin
連帶註冊 PlayerPoisonDamageEvent
)。這樣音效層只要注意事件,就能掌握遊戲當下的互動狀態。
音效播放統一放進 new_demo/src/plugins/audio.rs
:
impl Plugin for AudioPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, load_sound_effects).add_systems(
Update,
(
play_player_attack_sound,
play_enemy_attack_sound,
play_player_pickup_sound,
play_player_level_up_sound,
play_player_poison_damage_sound,
play_door_state_sound,
),
);
}
}
play_...
系統們都遵循相同做法:
EventReader
把排隊的事件掃過一遍,只要收到至少一筆,就觸發音效。spawn_one_shot(commands, handle)
,生成一個含 AudioPlayer
的 entity。PlaybackSettings::DESPAWN
會在音效播完後自動刪除該 entity,避免留下垃圾。另外補了一點細節是 spawn_one_shot
收到的是 Handle<AudioSource>
,只要 clone 一份就能交給 AudioPlayer::new
,Bevy 會自行決定幾時開始播、幾時收尾。這種做法很適合短音效,不需要額外管控播放狀態。
AudioPlugin
最後被安插在 main.rs
的 plugin 列表中,在 WorldPlugin
後、PlayerPlugin
前:
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_plugins((
WorldPlugin,
AudioPlugin,
PlayerPlugin,
UiPlugin,
EnemyPlugin,
ProgressionPlugin,
ItemPlugin,
ChestPlugin,
EquipmentPlugin,
CameraPlugin,
AttackPlugin,
WallCollisionPlugin,
DoorInteractionPlugin,
RoomTransitionPlugin,
))
.run();
這樣可以確保音效資源在玩家與敵人系統執行前就準備到位。PlayerPlugin
等也註冊了各自的事件,因此整個音效流程算是平行在主要邏輯旁邊運作。
因為要錄下目前遊戲的聲音有點麻煩,所以這篇文章就沒有 demo 的範例了,但是如果有興趣的話,可以到下方提供的專案連結到今天的進度,去執行後就可以實際操作,就可以體驗到:
音效架構改成事件導向後,要再增加 UI 點擊或音效就很簡單,只要新增事件與播放系統就好。
今日程式碼同步至 repo